Conversation
…A-f739-39g5-jj49)
WorkSpaceMemberViewSet.partial_update passed request.data verbatim into
WorkSpaceMemberSerializer (fields="__all__"), making workspace, role, and
is_active mass-assignable. A workspace admin could PATCH a member row setting
workspace=<victim UUID> and role=20, relocating a controlled account into the
victim workspace as admin — full cross-tenant takeover.
The endpoint's only legitimate mutation is `role`, so restrict the writable
payload to {"role": ...}. Note: passing fields=("id","member","role") does NOT
work — DynamicBaseSerializer discards the fields= kwarg (base.py) — so the
allowlist is enforced in the view instead. Preserves the self-role-update guard
and the guest role-cascade. is_active is only changed via destroy(), not here.
Adds 4 contract tests; fail-before verified (2 attack tests failed unpatched → 4 pass).
Co-authored-by: Plane AI <noreply@plane.so>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughWorkspace member partial updates now accept only ChangesWorkspace member update security
Priority: ⬆️ High Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The workspace-member PATCH change is mergeable after normal checks; no actionable risk remains identified. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change blocks workspace reassignment and activation through this role-update endpoint and makes related role changes atomic. No new security weakness is established, though concurrent membership changes remain an area of uncertainty. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Linked to Plane Work Item(s) This comment was auto-generated by Plane |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/api/plane/tests/contract/app/test_workspace_member_mass_assignment_app.py`:
- Around line 99-107: Extend the mixed-payload assertions in the workspace
member update test to verify that the permitted role change is applied by
asserting puppet_member.role equals 20 after refresh_from_db(), while retaining
the existing assertions that the workspace remains unchanged and no victim
member is created.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 65e256f5-7f4d-4580-8509-f4bf08174ff0
📒 Files selected for processing (2)
apps/api/plane/app/views/workspace/member.pyapps/api/plane/tests/contract/app/test_workspace_member_mass_assignment_app.py
There was a problem hiding this comment.
Pull request overview
This PR addresses a critical security vulnerability (GHSA-f739-39g5-jj49 / WEB-8332) where WorkSpaceMemberViewSet.partial_update allowed workspace admins to mass-assign sensitive WorkspaceMember fields (e.g. workspace, is_active) and perform cross-tenant workspace takeover by relocating a controlled member row into another workspace.
Changes:
- Restricts
WorkSpaceMemberViewSet.partial_updateto only acceptrolein the writable payload (view-level allowlist). - Adds contract regression tests covering the mass-assignment takeover vector and ensuring intended role updates still work.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| apps/api/plane/app/views/workspace/member.py | Adds a view-level allowlist to prevent mass-assignment of WorkspaceMember fields via PATCH. |
| apps/api/plane/tests/contract/app/test_workspace_member_mass_assignment_app.py | Adds contract regression tests to prevent cross-workspace moves and verify allowed role updates. |
…opilot #9460) partial_update cascaded the guest project-role downgrade BEFORE the serializer was validated/saved, using a manual int() cast that 500s on non-integer input. Reorder: validate the serializer first, then cascade using serializer.validated_data["role"] inside a transaction.atomic() with save(), so a bad role returns 400 (not 500) and project roles can't be downgraded when the member update doesn't persist. Adds a non-integer-role 400 test. Co-authored-by: Plane AI <noreply@plane.so>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/api/plane/tests/contract/app/test_workspace_member_mass_assignment_app.py`:
- Around line 162-179: Extend test_non_integer_role_is_400_not_500 by creating a
related ProjectMember for target_member with a non-guest role before the invalid
PATCH, then refresh it and assert its role remains unchanged alongside
target_member.role. Use the existing project-member creation helpers and role
symbols visible in the test module.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1339de50-6ecc-41a5-857f-0a45edaba3bb
📒 Files selected for processing (2)
apps/api/plane/app/views/workspace/member.pyapps/api/plane/tests/contract/app/test_workspace_member_mass_assignment_app.py
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/api/plane/app/views/workspace/member.py
…id update (CodeRabbit #9460) Address two CodeRabbit review comments: - Mixed-payload takeover test now also asserts the permitted field (role) in the same payload WAS applied (role == 20), proving the fix filters the payload rather than rejecting it wholesale. - test_non_integer_role_is_400_not_500 now seeds a non-guest ProjectMember and asserts it stays role 15 after the invalid PATCH, so a regression that let the guest project-role cascade run on a failed update would be caught. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ld (Copilot #9460) When a PATCH carries only forbidden keys (e.g. {"is_active": false}), allowed_data is empty and the previous code still called serializer.save() — a no-op write that bumped updated_at/updated_by and ran through the save path for nothing. Early-return the current serialized member (200) when there are no allowed fields, so a forbidden-only payload has no side effects. Strengthened the is_active test to assert updated_at is unchanged (fail-before verified). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Explanations kept unchanged; only the IDs are removed. Co-authored-by: Plane AI <noreply@plane.so>
|
React Doctor skipped this pull request — it changed no React files. Reviewed by React Doctor for commit |
…-member-mass-assignment
Summary
CRITICAL — fixes a cross-tenant workspace takeover (WEB-8332). Confirmed-vulnerable on
origin/preview.WorkSpaceMemberViewSet.partial_updatepassedrequest.datastraight intoWorkSpaceMemberSerializer(fields = "__all__", only nestedmemberread-only).workspace,role, andis_activewere therefore mass-assignable. An admin of an attacker-owned workspace could PATCH a member row settingworkspace=victim-workspace UUID androle=20 → relocate a controlled account into the victim workspace as an admin with no invitation. Since all authorization derives fromWorkspaceMember(workspace, member, role, is_active)rows, this is a full cross-tenant takeover.@allow_permission([ADMIN], level="WORKSPACE")authorizes against the URL slug but does not stop the write from moving the row elsewhere.Fix
The endpoint's only legitimate mutation is
role, so the writable payload is restricted to{"role": ...}in the view:Why not
fields=("id","member","role")?DynamicBaseSerializer.__init__pops thefields=kwarg and immediately overwrites it withself.expand(serializers/base.py), sofields=never restricts writes or output. The view-level allowlist is the reliable fix and is the only write path through this serializer (list/retrieve are read-only), so no other consumer is affected. The self-role-update guard and guest role-cascade are preserved;is_activeis only changed viadestroy().Tests
New
tests/contract/app/test_workspace_member_mass_assignment_app.py— 4 cases (cross-workspace move blocked;is_active=falseno-ops; legitimate role update works; self-update still 400). Fail-before verified on the CE docker test stack: 2 attack tests failed unpatched → 4 pass patched; 12/12 in a regression run with sibling member/authz suites.Related finding (separate ticket — NOT in this PR)
The root cause exposed a latent bug:
DynamicBaseSerializer'sfields=kwarg is discarded (base.py:fields = self.expand), so every caller that passesfields=(...)expecting to limit output (e.g.list/retrievehere) actually returns all model columns. That's an app-wide over-disclosure surface needing its own scoped audit + FE-compat check — filed separately; deliberately not touched here (changingbase.pywould enforce output filtering app-wide and likely break the frontend).Summary by CodeRabbit
Bug Fixes
rolechange; requests without a role leave the member unchanged.Tests